﻿/**
* The template class allows you to keep your HTML code in some external files
* which are completely free of ASP(JScript) code, but contain replacement fields.
* The class provides you with functions which can fill in the replacement fields
* with arbitrary strings. These strings can become very large, e.g. entire tables.
*
* Note: If you think that this is like phplib Templates, read carefully. It isn't.
*
*/
/******************************************************************************
* Class constructor. May be called with two optional parameters.
* The first parameter sets the template directory the second parameter
* sets the policy regarding handling of unknown variables.
*
* usage: Template([string root = "."], [string unknowns = "remove"])
*
* @param     root        path to template directory
* @param     string      what to do with undefined variables
* @see       set_root
* @see       set_unknowns
* @access    public
* @return    void
*/
function Template(root, unknowns) {
    //config
    this.version = "1.0.0.0";
    this.name = "Template";
    this.debug = 0;
    this.root = root || ".";
    this.unknowns = unknowns || "remove"; //remove|keep|......
    this.on_error_on_error = "no"; //yes|no|report
    this.last_error = "";
    this.charset = "auto";
    this._prefix = "{[";
    this._suffix = "]}";
    this._varname = "([~!@#\$%\^&\*_=-]?[\\w\\d\\u0100-\\uFFFF_-]+)";
    /*****************************************************************************************************************/
    /** 
    * 设置标签前缀
    * @param p 
    */
    this.set_prefix = function(p) {
        this._suffix = p || this._prefix;
        this.reg_var.compile(this.encode(this._prefix) + this._varname + this.encode(this._suffix), "g");
    };
    /** 
    * 设置标签后缀
    * @param s
    */
    this.set_suffix = function(s) {
        this._suffix = s || this._suffix;
        this.reg_var.compile(this.encode(this._prefix) + this._varname + this.encode(this._suffix), "g");
    };
    /******************************************************************************
    * Checks that root is a valid directory and if so sets this directory as the
    * base directory from which templates are loaded by storing the value in
    * this.root. Relative filenames are prepended with the path in this.root.
    *
    * Returns true on success, false on error.
    *
    * usage: set_root(string root)
    *
    * @param     root         string containing new template directory
    * @see       root
    * @access    public
    * @return    boolean
    */
    this.set_root = function(root) {
        if (!this.is_dir(root)) {
            this.on_error(new Error("set_root:[" + root + "] is not a directory."));
            return false;
        }
        this.root = root;
        return true;
    };
    /******************************************************************************
    * Sets the policy for dealing with unresolved variable names.
    *
    * unknowns defines what to do with undefined template variables
    * "remove"   = remove undefined variables
    * "comment"  = replace undefined variables with comments
    * "keep"     = keep undefined variables
    *
    * Note: "comment" can cause unexpected results when the variable tag is embedded
    * inside an HTML tag, for example a tag which is expected to be replaced with a URL.
    *
    * usage: set_unknowns(string unknowns)
    *
    * @param     unknowns         new value for unknowns
    * @see       unknowns
    * @access    public
    * @return    void
    */
    this.set_unknowns = function(unknowns) {
        this.unknowns = unknowns;
    };
    /******************************************************************************
    * Defines a filename for the initial value of a variable.
    *
    * The new mappings are stored in the array $this.file.
    * The files are not loaded yet, but only when needed.
    *
    * Returns true on success, false on error.
    *
    * usage: set_file(string varname, string filename)
    *
    * @param     varname      a string containing a varname ;
    * @param     filename     a string containing the filename
    * @access    public
    * @return    boolean
    */
    this.set_file = function(handle, filename) {
        filename = this.fso.BuildPath(this.root, filename);
        if (!this.fso.FileExists(filename)) {
            this.on_error(new Error("filename:file [" + filename + "] does not exist."));
            return false;
        }
        this.map_file["$" + handle] = filename;
        return true;
    };
    /******************************************************************************
    * This functions sets the value of a variable.
    * The function inserts the new value of the variable into the
    * map_var hashes. It is not necessary for a variable to exist in these hashes
    * before calling this function.
    *
    * An optional third parameter allows the value for each varname to be appended
    * to the existing variable instead of replacing it. The default is to replace.
    * This feature was introduced after the 7.2d release.
    *
    *
    * usage: set_var(string varname, [string value = ""], [boolean append = false])
    * @param     varname      a string containing a varname
    * @param     value        a string this contains the new value for the variable
    * @param     append       if true, the value is appended to the variable's existing value
    * @access    public
    * @return    void
    */
    this.set_var = function(varname, value, append) {
        varname = "$" + varname;
        if (append) {
            this.map_var[varname] = (this.map_var[varname] || "") + value;
        } else {
            this.map_var[varname] = value;
        }
    };
    this.split = function(prefix) {
        for (var key in this.map_file) {
            this.set_blocks(key.substr(1), prefix);
        }
    };
    this.set_blocks = function(handle, prefix) {
        if (!this.loadfile(handle)) {
            this.on_error(new Error("subst:unable to load [" + handle + "]"));
        }
        var str = this.get_var(handle);
        arguments[1] = prefix || "$";
        str = str.replace(this.reg_black, this._set_blocks_call);
        this.set_var(handle, str);
    };
    this._set_blocks_call = function(block, S1, handle, value) {
        var that = arguments.callee.that, prefix = arguments.caller[1];
        that.set_var(handle, value);
        that.set_blocks(handle, prefix);
        return S1 + that._prefix + prefix + handle + that._suffix;
    };
    /******************************************************************************
    * A variable parent may contain a variable block defined by:
    * &lt;!-- BEGIN $varname --&gt; content &lt;!-- END varname --&gt;. This function removes
    * that block from parent and replaces it with a variable reference named name.
    * The block is inserted into the varkeys and map_var hashes. If name is
    * omitted, it is assumed to be the same as varname.
    *
    * Blocks may be nested but care must be taken to extract the blocks in order
    * from the innermost block to the outermost block.
    *
    * Returns true on success, false on error.
    *
    * usage: set_block(string parent, string varname, [string name = ""])
    *
    * @param     parent       a string containing the name of the parent variable
    * @param     varname      a string containing the name of the block to be extracted
    * @param     name         the name of the variable in which to store the block
    * @access    public
    * @return    boolean
    */
    this.set_block = function(parent, handle, varname) {
        if (!this.loadfile(parent)) {
            this.on_error(new Error("subst:unable to load [" + parent + "]"));
        }
        varname = varname || ("_" + handle);
        var str = this.get_var(parent);
        var re = new RegExp("(<\!--\\s+BEGIN\\s+(" + handle + ")\\s+-->)([\\s\\S.]*?)(?=<\!--\\s+END\\s+\\2\\s+-->)", "g");
        var arr = re.exec(str);
        if (arr) {
            this.set_var(handle, arr[3]);
        }
        str = str.replace(re, "$1" + this._prefix + varname + this._suffix);
        this.set_var(parent, str);
        return !!arr;
    };
    /** 
    * 进行变量替换 
    * @param handle 
    * @return 
    */
    this.subst = function(handle) {
        arguments.that = this;
        if (!this.loadfile(handle)) {
            this.on_error(new Error("subst:unable to load [" + handle + "]"));
        }
        var str = this.get_var(handle);
        str = str.replace(this.reg_var, this._subst_call);
        return str;
    };
    this.psubst = function(handle) {
        Response.Write(this.subst(varname));
    };
    this._subst_call = function(tag, varname) {
        var that = arguments.callee.that;
        varname = "$" + varname;
        if (varname in that.map_var) {
            return that.map_var[varname];
        }
        return tag;
    };
    /******************************************************************************
    * Returns: the last value assigned to target.
    *
    * usage: parse(string target, string varname, [boolean append])
    * or
    * usage: parse(string target, array varname = (string varname), [boolean append])
    *
    * @param     target      a string containing the name of the variable into which substituted varnames are to be stored
    * @param     varname     if a string, the name the name of the variable to substitute or if an array a list of variables to be substituted
    * @param     append      if true, the substituted variables are appended to target otherwise the existing value of target is replaced
    * @access    public
    * @return    string
    * @see       subst
    */
    this.parse = function(target, handle, append) {
        var str;
        if (handle instanceof Array) {
            for (var i = 0; i < handle.length; i++) {
                str = this.subst(handle[i]);
                if (append) {
                    str = (this.get_var(target) || "") + str;
                }
                this.set_var(target, str);
            }
        } else {
            str = this.subst(handle);
            if (append) {
                str = (this.get_var(target) || "") + str;
            }
            this.set_var(target, str);
        }
        return str;
    };
    /******************************************************************************
    * usage: p(string varname)
    *
    * @param     $varname     a string containing the name of the variable to finish and print
    * @access    public
    * @return    void
    * @see       set_unknowns
    * @see       finish
    */
    this.p = function(varname) {
        Response.Write(this.finish(this.get_var(varname)));
    };
    /******************************************************************************
    * Returns: always returns false.
    *
    * usage: pparse(string target, string varname, [boolean append])
    * or
    * usage: pparse(string target, array varname = (string varname), [boolean append])
    *
    * @param     target      a string containing the name of the variable into which substituted varnames are to be stored
    * @param     varname     if a string, the name the name of the variable to substitute or if an array a list of variables to be substituted
    * @param     append      if true, the substituted variables are appended to target otherwise the existing value of target is replaced
    * @access    public
    * @return    false
    * @see       parse
    */
    this.pparse = function(target, handle, append) {
        this.parse(target, handle, append);
        Response.Write(this.finish(this.get_var(target)));
    };
    /** 
    * 加载模板文件 
    * @param handle 
    * @return 
    */
    this.loadfile = function(handle) {
        var s, filename, varname = "$" + handle;
        if (varname in this.map_var && this.map_var[varname] != null) {
            return true;
        }
        if (!(varname in this.map_file)) {
            this.on_error(new Error("loadfile:[" + handle + "] is not a valid handle."));
            return false;
        }
        filename = this.map_file[varname];
        if (!this.fso.FileExists(filename)) {
            this.on_error(new Error("loadfile:while loading [" + handle + "],[" + filename + "] does not exist."));
            return false;
        }
        try {
            with (this.stm) {
                Type = 2;
                Mode = 3;
                CharSet = "iso-8859-1";
                Open();
                LoadFromFile(filename);
                if (!this.charset || this.charset.toLowerCase() == "auto") {
                    s = ReadText(4);
                    Position = 0;
                    if (s.substr(0, 3) == "\xEF\xBB\xBF") {
                        CharSet = "utf-8";
                    } else if (s.substr(0, 2) == "\xFF\xFE") {
                        CharSet = "utf-16le";
                    } else if (s.substr(0, 2) == "\xFE\xFF") {
                        CharSet = "utf-16be";
                    } else if (s == "\x84\x31\x95\x33") {
                        CharSet = "gb18030";
                    } else {
                        CharSet = "_autodetect_all";
                    }
                } else {
                    CharSet = this.charset;
                }
                s = ReadText();
                Close();
            }
            if (s == "") {
                this.on_error(new Error("loadfile:while loading [" + handle + "],[" + filename + "] is empty."));
                return false;
            }
            this.set_var(handle, s);
        }
        catch (e) {
            this.on_error(e);
        }
        return true;
    };
    /** 
    * 获取变量 (获得标签值)
    * @param varname 
    * @return 
    */
    this.get_var = function(varname) {
        var value = [];
        if (varname instanceof Array) {
            for (var i = 0, v; i < varname.length; i++) {
                varname[i] = "$" + varname[i];
                value.push(varname[i] in this.map_var ? this.map_var[varname[i]] : "");
            }
        } else {
            varname = "$" + varname;
            value = (varname in this.map_var ? this.map_var[varname] : "");
        }
        return value;
    };
    /** 
    * 判断目录 (判断目录是否存在)
    * @param path 
    * @return 
    */
    this.is_dir = function(path) {
        return this.fso.FolderExists(path);
    };
    /** 
    * 正则表达式特殊字符编码
    * @param s
    * @return
    */
    this.encode = function(s) {
        s = s.replace(/\\/mg, "\\\\");
        s = s.replace(/[\$\(\)\*\+\.\[\]\?\^\{\}\|\/]/mg, "\\$&");
        s = s.replace(/\n/mg, "\\n");
        s = s.replace(/\r/mg, "\\r");
        s = s.replace(/\t/mg, "\\t");
        return s;
    };
    /** 
    * 完成字符串的处理 (处理不明标签)
    * @param str 
    * @return 
    */
    this.finish = function(str) {
        switch (this.unknowns) {
            case "remove":
                str = str.replace(this.reg_var, "");
                break;
            case "keep":
                break;
            default:
                str = str.replace(this.reg_var, this.unknowns);
                break;
        }
        return str;
    };
    this.get_undefined = function(varname) {
        if (!this.loadfile(varname)) {
            this.on_error(new Error("get_undefined:unable to load [" + varname + "]"));
            return;
        }
        var re = new RegExp("(?!" + this.encode(this._prefix) + ")" + this._varname + "(?=" + this.encode(this._suffix) + ")", "g");
        var m = this.get_var(varname).match(re);
        var r = new Object(), l = 0;
        if (m) {
            for (var i = 0; i < m.length; i++) {
                if (!(m[i] in this.map_var)) {
                    r[m[i]] = m[i];
                    l++;
                }
            }
        }
        return l ? r : undefined;
    };
    this.get = function(varname) {
        return this.finish(this.get_var(varname));
    };

    this.get_vars = function() {
        var r = new Object();
        for (var k in this.map_var) {
            r[k] = this.map_var[k];
        }
        return r;
    };
    this.unset_var = function(varname) {
        if (varname instanceof Array) {
            for (var i = 0, v; i < varname.length; i++) {
                delete this.map_var["$" + varname[i]];
            }
        } else {
            delete this.map_var["$" + varname];
        }
    };
    this.clear_var = function(varname) {
        if (varname instanceof Array) {
            for (var i = 0, v; i < varname.length; i++) {
                this.map_var["$" + varname[i]] = "";
            }
        } else {
            this.map_var["$" + varname] = "";
        }
    };
    this.on_error = function(er) {
        this.last_error = er;
        if (this.debug) {
            Response.Write("<\!--Template Error:" + er.description + "-->\n");
        }
        if (this.on_error_on_error == "yes") {
            Response.Write("<\!--halted.-->\n");
            Response.End();
        }
    };
    /*****************************************************************************************************************/
    this.map_file = new Object();
    this.map_var = new Object();
    this.fso = new ActiveXObject("Scripting.FileSystemObject");
    this.stm = new ActiveXObject("Adodb.Stream");
    this.time = (new Date()).valueOf();
    this.reg_var = new RegExp();
    this.reg_black = new RegExp();
    this.reg_var.compile(this.encode(this._prefix) + this._varname + this.encode(this._suffix), "g");
    this.reg_black.compile("(<\!--\\s+BEGIN\\s+" + this._varname + "\\s+-->)([\\s\\S.]*?)(?=<\!--\\s+END\\s+\\2\\s+-->)", "g");
    this._subst_call.that = this;
    this._set_blocks_call.that = this;
    this.root = Server.MapPath(this.root);
}